package btp

import (
	"encoding/binary"
	"fmt"
	"net/url"
	"strings"
)

type DataType byte

const (
	DUint8 DataType = iota + 1
	DInt8
	DUint16
	DInt16
	DUint32
	DInt32
	DUint64
	DInt64
	DString
	DBigString
)

func (v DataType) String() string {
	switch v {
	case DUint8:
		return "Uint8"
	case DInt8:
		return "Int8"
	case DUint16:
		return "Uint16"
	case DInt16:
		return "Int16"
	case DUint32:
		return "Uint32"
	case DInt32:
		return "Int32"
	case DUint64:
		return "Uint64"
	case DInt64:
		return "Int64"
	case DString:
		return "String"
	case DBigString:
		return "BigString"
	default:
		return ""
	}
}

//

type Fields struct {
	list []*Field
}

func NewFields() *Fields {
	return &Fields{list: []*Field{}}
}

func (fs *Fields) Fields() []*Field { return fs.list }

func (fs *Fields) Encode() []byte {
	size := 0
	for _, f := range fs.list {
		size += f.encodeSize()
	}
	b := make([]byte, size)
	i := 0
	for _, f := range fs.list {
		sz := f.encodeSize()
		copy(b[i:i+sz], f.encode())
		i += sz
	}
	return b
}

func (fs *Fields) Decode(b []byte) error {
	p := &parser{b: b}
	list := []*Field{}
	for p.remain() > 0 {
		field, err := p.readField()
		if err != nil {
			return err
		}
		list = append(list, field)
	}
	fs.list = list
	return nil
}

type ErrUndefinedField struct {
	Field string
}

func (e *ErrUndefinedField) Error() string {
	return fmt.Sprintf("undefined field: %s", e.Field)
}

type ErrWrongDataType struct {
	Field    string
	DataType DataType
}

func (e *ErrWrongDataType) Error() string {
	return fmt.Sprintf("wrong data type: %s must be %s", e.Field, e.DataType.String())
}

func (fs *Fields) AddUint8(field string, value uint8) error {
	info := getFieldByName(field)
	if info == nil {
		return &ErrUndefinedField{Field: field}
	}
	if info.dtype != DUint8 {
		return &ErrWrongDataType{Field: field, DataType: info.dtype}
	}
	f := &Field{info: info}
	f.data = []byte{value}
	fs.list = append(fs.list, f)
	return nil
}

func (fs *Fields) AddInt8(field string, value int8) error {
	info := getFieldByName(field)
	if info == nil {
		return &ErrUndefinedField{Field: field}
	}
	if info.dtype != DInt8 {
		return &ErrWrongDataType{Field: field, DataType: info.dtype}
	}
	f := &Field{info: info}
	f.data = []byte{byte(value)}
	fs.list = append(fs.list, f)
	return nil
}

func (fs *Fields) AddUint16(field string, value uint16) error {
	info := getFieldByName(field)
	if info == nil {
		return &ErrUndefinedField{Field: field}
	}
	if info.dtype != DUint16 {
		return &ErrWrongDataType{Field: field, DataType: info.dtype}
	}
	f := &Field{info: info}
	f.data = make([]byte, 2)
	binary.BigEndian.PutUint16(f.data, value)
	fs.list = append(fs.list, f)
	return nil
}

func (fs *Fields) AddInt16(field string, value int16) error {
	info := getFieldByName(field)
	if info == nil {
		return &ErrUndefinedField{Field: field}
	}
	if info.dtype != DInt32 {
		return &ErrWrongDataType{Field: field, DataType: info.dtype}
	}
	f := &Field{info: info}
	f.data = make([]byte, 2)
	binary.BigEndian.PutUint16(f.data, uint16(value))
	fs.list = append(fs.list, f)
	return nil
}

func (fs *Fields) AddUint32(field string, value uint32) error {
	info := getFieldByName(field)
	if info == nil {
		return &ErrUndefinedField{Field: field}
	}
	if info.dtype != DUint32 {
		return &ErrWrongDataType{Field: field, DataType: info.dtype}
	}
	f := &Field{info: info}
	f.data = make([]byte, 4)
	binary.BigEndian.PutUint32(f.data, value)
	fs.list = append(fs.list, f)
	return nil
}

func (fs *Fields) AddInt32(field string, value int32) error {
	info := getFieldByName(field)
	if info == nil {
		return &ErrUndefinedField{Field: field}
	}
	if info.dtype != DInt32 {
		return &ErrWrongDataType{Field: field, DataType: info.dtype}
	}
	f := &Field{info: info}
	f.data = make([]byte, 4)
	binary.BigEndian.PutUint32(f.data, uint32(value))
	fs.list = append(fs.list, f)
	return nil
}

func (fs *Fields) AddUint64(field string, value uint64) error {
	info := getFieldByName(field)
	if info == nil {
		return &ErrUndefinedField{Field: field}
	}
	if info.dtype != DUint64 {
		return &ErrWrongDataType{Field: field, DataType: info.dtype}
	}
	f := &Field{info: info}
	f.data = make([]byte, 8)
	binary.BigEndian.PutUint64(f.data, value)
	fs.list = append(fs.list, f)
	return nil
}

func (fs *Fields) AddInt64(field string, value int64) error {
	info := getFieldByName(field)
	if info == nil {
		return &ErrUndefinedField{Field: field}
	}
	if info.dtype != DInt64 {
		return &ErrWrongDataType{Field: field, DataType: info.dtype}
	}
	f := &Field{info: info}
	f.data = make([]byte, 8)
	binary.BigEndian.PutUint64(f.data, uint64(value))
	fs.list = append(fs.list, f)
	return nil
}

func (fs *Fields) AddString(field string, value string) error {
	info := getFieldByName(field)
	if info == nil {
		return &ErrUndefinedField{Field: field}
	}
	if info.dtype != DString {
		return &ErrWrongDataType{Field: field, DataType: info.dtype}
	}
	f := &Field{info: info}
	f.data = []byte(value)
	fs.list = append(fs.list, f)
	return nil
}

func (fs *Fields) AddBigString(field string, value string) error {
	info := getFieldByName(field)
	if info == nil {
		return &ErrUndefinedField{Field: field}
	}
	if info.dtype != DBigString {
		return &ErrWrongDataType{Field: field, DataType: info.dtype}
	}
	f := &Field{info: info}
	f.data = []byte(value)
	fs.list = append(fs.list, f)
	return nil
}

//

func Decode(b []byte) (*Fields, error) {
	fs := NewFields()
	if err := fs.Decode(b); err != nil {
		return nil, err
	}
	return fs, nil
}

func (fs *Fields) FormatUrl() string {
	ss := make([]string, len(fs.list))
	for i, f := range fs.list {
		var s string
		switch f.info.dtype {
		case DUint8:
			v, _ := f.Uint8()
			s = fmt.Sprint(v)
		case DInt8:
			v, _ := f.Int8()
			s = fmt.Sprint(v)
		case DUint16:
			v, _ := f.Uint16()
			s = fmt.Sprint(v)
		case DInt16:
			v, _ := f.Int16()
			s = fmt.Sprint(v)
		case DUint32:
			v, _ := f.Uint32()
			s = fmt.Sprint(v)
		case DInt32:
			v, _ := f.Int32()
			s = fmt.Sprint(v)
		case DUint64:
			v, _ := f.Uint64()
			s = fmt.Sprint(v)
		case DInt64:
			v, _ := f.Int64()
			s = fmt.Sprint(v)
		case DString:
			s, _ = f.String()
		case DBigString:
			s, _ = f.BigString()
		}
		ss[i] = f.info.name + "=" + url.QueryEscape(s)
	}
	return strings.Join(ss, "&")
}
